译:Implementing a CNN for Text Classification in TensorFlow

在TensorFlow中实现CNN进行文本分类

Implementing a CNN for Text Classification in TensorFlow
原文链接:http://www.wildml.com/2015/12/implementing-a-cnn-for-text-classification-in-tensorflow/
github代码:https://github.com/dennybritz/cnn-text-classification-tf

Implementing a CNN for Text Classification in TensorFlow

在这篇文章中,我们将实现一个类似于Kim Yoon的用于句子分类的卷积神经网络的模型。本文提出的模型在一系列文本分类任务(如情感分析)中实现了良好的分类性能,并已成为新文本分类体系结构的标准基线。

我假设您已经熟悉应用于NLP的卷积神经网络的基础知识。如果没有,我建议首先阅读了解NLP的卷积神经网络,以获得必要的背景知识。

Data and Preprocessing

我们将在这篇文章中使用的数据集是来自烂番茄的电影评论数据 - 原始论文中也使用的数据集之一。 数据集包含10,662个示例评论句子,一半正例和一半负例。 数据集的大小约为20k。 请注意,由于此数据集非常小,我们可能会过度使用强大的模型。 此外,数据集没有官方训练集和测试集的拆分,因此我们只使用10%的数据作为开发集。 原始论文报告了对数据进行10倍交叉验证的结果。

我不会在这篇文章中讨论数据预处理代码,但它可以在Github上获得并执行以下操作:

  • 加载数据:从原始数据集中加载正例和负例的句子
  • 数据清洗使用源论文的代码清洗文本数据
  • 数据填充将每个句子填充到最大句子长度59.我们将特殊的标记附加到所有其他句子,使它们成为59个单词。 将句子填充到相同长度是很有必要的,因为它允许我们有效地批量处理我们的数据,因为批处理中的每个示例必须具有相同的长度。
  • 构建句子向量构建词汇索引并将每个单词映射到0到18,765之间的整数(词汇量大小)。 使每个句子都成为一个整数的向量。

The Model

网络的模型如下:
model

第一层将单词嵌入到低维向量中。下一层使用多个滤波器大小对嵌入的字向量执行卷积。例如,一次滑动3个,4个或5个字。接下来,我们将卷积层的结果最大化为长特征向量,添加dropout正则化,并使用softmax层对结果进行分类。

因为这是一篇教程性质的文章,所以我决定简化原始论文的模型:

  • 我们不会使用word2vec预先训练好的词向量,相反,我们会从头开始学习如果训练词向量。
  • 我们不会对权重向量强制执行L2范数约束。 对句子分类的卷积神经网络的敏感性分析发现,约束对最终结果影响不大。
  • 原始论文用两个输入数据通道进行实验 - 静态和非静态词向量。 我们只使用一个通道。

向代码中添加上述扩展内容还是挺简单的,大概需要十几行代码完成

Let’s get started!

Implementation

为了允许各种超参数配置,我们将代码放入一个TextCNN类中,在init函数中生成模型图。

1
2
3
4
5
6
7
8
9
10
11
12
import tensorflow as tf
import numpy as np

class TextCNN(object):
"""
A CNN for text classification.
Uses an embedding layer, followed by a convolutional, max-pooling and softmax layer.
"""
def __init__(
self, sequence_length, num_classes, vocab_size,
embedding_size, filter_sizes, num_filters):
# Implementation...

为了实例化该类,我们传递以下参数:

  • sequence_length - 我们句子的长度。请记住,我们将所有句子填充为相同的长度(我们的数据集为59)。
  • num_classes - 输出层中的类数,在我们的数据中为两个(正例和负例)。
  • vocab_size - 我们词汇量的大小。这需要定义词向量的大小,嵌入层的shape为[vocabulary_size, embedding_size]。
  • embedding_size - 词向量的维度。
  • filter_sizes - 卷积过滤器覆盖的单词数。
  • num_filters - 每个卷积过滤器的数量。

Input Placeholders

我们首先定义传递给网络的输入数据:

1
2
3
4
# Placeholders for input, output and dropout
self.input_x = tf.placeholder(tf.int32, [None, sequence_length], name="input_x")
self.input_y = tf.placeholder(tf.float32, [None, num_classes], name="input_y")
self.dropout_keep_prob = tf.placeholder(tf.float32, name="dropout_keep_prob")

tf.placeholder创建一个占位符变量,当我们在训练或测试时执行它时,我们将其提供给网络。第二个参数是输入向量的形状。None意味着该维度的长度可以是任意值。在我们的例子中,第一个维度是批量大小,并且使用None允许网络处理任意大小的批次。

在dropout层中保留神经元的概率也是网络的输入,因为我们仅在训练期间启用dropout。我们在评估模型时禁用它(稍后会详细介绍)。

Embedding Layer

我们定义的第一层是embedding 层,它将词汇词索引映射到低维矢量表示。它本质上是一个从数据中学习的查找表。

1
2
3
4
5
6
with tf.device('/cpu:0'), tf.name_scope("embedding"):
W = tf.Variable(
tf.random_uniform([vocab_size, embedding_size], -1.0, 1.0),
name="W")
self.embedded_chars = tf.nn.embedding_lookup(W, self.input_x)
self.embedded_chars_expanded = tf.expand_dims(self.embedded_chars, -1)

我们在这里使用了几个新功能,让我们来看看它们:

  • tf.device(“/cpu:0”)强制在CPU上执行操作。默认情况下,如果有可用的话,TensorFlow会尝试将操作放在GPU上,但embedding实现目前没有GPU支持,如果置于GPU上则会引发错误。
  • tf.name_scope创建一个名为“embedding” 的新名称范围。范围将所有操作添加到名为“嵌入”的顶级节点中,以便在TensorBoard中可视化网络时获得良好的层次结构。

W是我们在训练期间学习的嵌入矩阵。我们使用随机均匀分布对其进行初始化。tf.nn.embedding_lookup创建实际的embedding操作。embedding操作的结果是三维张量[None, sequence_length, embedding_size]。

TensorFlow的卷积转换操作需要一个4维张量,其尺寸对应于batch,width,height和channel。我们嵌入的结果不包含通道尺寸,因此我们手动添加它,最后的shape为[None, sequence_length, embedding_size, 1]。

Convolution and Max-Pooling Layers

现在我们已经准备好构建我们的卷积层,然后是max-pooling。请记住,我们使用不同大小的过滤器。因为每个卷积产生不同形状的张量,我们需要迭代它们,为它们中的每一个创建一个层,然后将结果合并为一个大的特征向量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
pooled_outputs = []
for i, filter_size in enumerate(filter_sizes):
with tf.name_scope("conv-maxpool-%s" % filter_size):
# Convolution Layer
filter_shape = [filter_size, embedding_size, 1, num_filters]
W = tf.Variable(tf.truncated_normal(filter_shape, stddev=0.1), name="W")
b = tf.Variable(tf.constant(0.1, shape=[num_filters]), name="b")
conv = tf.nn.conv2d(
self.embedded_chars_expanded,
W,
strides=[1, 1, 1, 1],
padding="VALID",
name="conv")
# Apply nonlinearity
h = tf.nn.relu(tf.nn.bias_add(conv, b), name="relu")
# Max-pooling over the outputs
pooled = tf.nn.max_pool(
h,
ksize=[1, sequence_length - filter_size + 1, 1, 1],
strides=[1, 1, 1, 1],
padding='VALID',
name="pool")
pooled_outputs.append(pooled)

# Combine all the pooled features
num_filters_total = num_filters * len(filter_sizes)
self.h_pool = tf.concat(3, pooled_outputs)
self.h_pool_flat = tf.reshape(self.h_pool, [-1, num_filters_total])

这里W是我们的滤波器矩阵,h是将非线性应用于卷积输出的结果。每个过滤器都滑过整个embedding,但它覆盖的单词数量会有所不同。”VALID”padding意味着我们将过滤器滑过句子而不填充边缘,执行一个窄卷积,输出shape指定为[1, sequence_length - filter_size + 1, 1, 1]。在特定过滤器尺寸的输出上执行最大池化使我们具有张量形状[batch_size, 1, 1, num_filters]。这本质上是一个特征向量,其中最后一个维度对应于我们的特征。一旦我们从每个滤波器大小获得所有合并的输出张量,我们将它们组合成一个长形状的特征向量[batch_size, num_filters_total]。使用-1in tf.reshape告诉TensorFlow尽可能展平尺寸。

Dropout Layer

dropout可能是最常用的规则化卷积神经网络的方法。dropout背后的想法很简单。dropout层随机“禁用”其神经元的一部分。这可以防止神经元共同适应并迫使它们学习各自有用的特征。我们保持启用的神经元部分由dropout_keep_prob网络的输入定义。我们在训练期间将其设置为0.5,在评估期间设置为1(禁用dropout)。

1
2
3
# Add dropout
with tf.name_scope("dropout"):
self.h_drop = tf.nn.dropout(self.h_pool_flat, self.dropout_keep_prob)

Scores and Predictions

使用max-pooling中的特征向量(应用了dropout),我们可以通过矩阵乘法和选择具有最高分数的类来生成预测。我们还可以应用softmax函数将原始分数转换为标准化概率,但这不会改变我们的最终预测。

1
2
3
4
5
with tf.name_scope("output"):
W = tf.Variable(tf.truncated_normal([num_filters_total, num_classes], stddev=0.1), name="W")
b = tf.Variable(tf.constant(0.1, shape=[num_classes]), name="b")
self.scores = tf.nn.xw_plus_b(self.h_drop, W, b, name="scores")
self.predictions = tf.argmax(self.scores, 1, name="predictions")

这里tf.nn.xw_plus_b执行Wx + b矩阵乘法。

Loss and Accuracy

使用我们的Scores,我们可以定义损失函数。损失是衡量我们网络错误的指标,我们的目标是最小化它。分类的标准损失函数问题是交叉熵损失。

1
2
3
4
# Calculate mean cross-entropy loss
with tf.name_scope("loss"):
losses = tf.nn.softmax_cross_entropy_with_logits(self.scores, self.input_y)
self.loss = tf.reduce_mean(losses)

这里,tf.nn.softmax_cross_entropy_with_logits是一个函数,它根据我们的Scores和正确的输入标签计算每个类的交叉熵损失。然后我们采取损失的平均值。我们也可以使用总和,但这使得比较不同批量大小和训练/开发数据的损失变得更加困难。

我们还定义了精度的表达式,这是在训练和测试期间跟踪的有用数量。

1
2
3
4
# Calculate Accuracy
with tf.name_scope("accuracy"):
correct_predictions = tf.equal(self.predictions, tf.argmax(self.input_y, 1))
self.accuracy = tf.reduce_mean(tf.cast(correct_predictions, "float"), name="accuracy")

Training Procedure

之前我们定义为我们的网络训练过程中,我们需要了解TensorFlow如何使用一些基本的Sessions和Graphs。如果您已经熟悉这些概念,请随意跳过本节。

在TensorFlow中,a Session是您正在执行图形操作的环境,它包含有关变量和队列的状态。每个会话都在一个图表上运行。如果在创建变量和操作时未显式使用会话,则使用TensorFlow创建的当前默认会话。您可以通过执行session.as_default()块内的命令来更改默认会话(请参阅下文)。

A Graph包含操作和张量。您可以在程序中使用多个图形,但大多数程序只需要一个图形。您可以在多个会话中使用相同的图形,但不能在一个会话中使用多个图形。TensorFlow始终创建默认图形,但您也可以手动创建图形并将其设置为新默认图形,如下所示。显式创建会话和图表可确保在不再需要资源时正确释放资源。

1
2
3
4
5
6
7
with tf.Graph().as_default():
session_conf = tf.ConfigProto(
allow_soft_placement=FLAGS.allow_soft_placement,
log_device_placement=FLAGS.log_device_placement)
sess = tf.Session(config=session_conf)
with sess.as_default():
# Code that operates on the default graph and session comes here...

allow_soft_placement设置允许TensorFlow回落的设备上时,优选的设备不存在实现的某些操作。例如,如果我们的代码在GPU上进行操作,并且我们在没有GPU的机器上运行代码,则不使用allow_soft_placement会导致错误。如果设置了log_device_placement,TensorFlow会记录它放置操作的设备(CPU或GPU)。这对调试很有用。FLAGS是我们程序的命令行参数。

Instantiating the CNN and minimizing the loss

当我们实例化我们的TextCNN模型时,所有定义的变量和操作将被放入我们上面创建的默认Graphs和Sessions中。

1
2
3
4
5
6
7
cnn = TextCNN(
sequence_length=x_train.shape[1],
num_classes=2,
vocab_size=len(vocabulary),
embedding_size=FLAGS.embedding_dim,
filter_sizes=map(int, FLAGS.filter_sizes.split(",")),
num_filters=FLAGS.num_filters)

接下来,我们定义如何优化网络的损耗函数。TensorFlow有几个内置的优化器。我们正在使用的是Adam优化器。

1
2
3
4
global_step = tf.Variable(0, name="global_step", trainable=False)
optimizer = tf.train.AdamOptimizer(1e-4)
grads_and_vars = optimizer.compute_gradients(cnn.loss)
train_op = optimizer.apply_gradients(grads_and_vars, global_step=global_step)

这里,train_op这是一个新创建的操作,我们可以运行它来对我们的参数执行渐变更新。每次执行train_op都是一个训练步骤。TensorFlow自动确定哪些变量是“可训练的”并计算其梯度。通过定义global_step变量并将其传递给优化器,我们允许TensorFlow为我们处理训练步骤的计数。每次执行时,train_op全局步骤将自动递增1 。

Summaries

TensorFlow具有Summaries概念,允许您在培训和评估期间跟踪和可视化各种变量。例如,您可能希望跟踪损失和准确度随时间的变化情况。您还可以跟踪更复杂的数量,例如图层激活的直方图。摘要是序列化对象,它们使用SummaryWriter写入磁盘。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Output directory for models and summaries
timestamp = str(int(time.time()))
out_dir = os.path.abspath(os.path.join(os.path.curdir, "runs", timestamp))
print("Writing to {}\n".format(out_dir))

# Summaries for loss and accuracy
loss_summary = tf.scalar_summary("loss", cnn.loss)
acc_summary = tf.scalar_summary("accuracy", cnn.accuracy)

# Train Summaries
train_summary_op = tf.merge_summary([loss_summary, acc_summary])
train_summary_dir = os.path.join(out_dir, "summaries", "train")
train_summary_writer = tf.train.SummaryWriter(train_summary_dir, sess.graph_def)

# Dev summaries
dev_summary_op = tf.merge_summary([loss_summary, acc_summary])
dev_summary_dir = os.path.join(out_dir, "summaries", "dev")
dev_summary_writer = tf.train.SummaryWriter(dev_summary_dir, sess.graph_def)

在这里,我们分别跟踪训练和评估的Summaries。tf.merge_summary是一个便捷函数,它将多个汇总操作合并为一个我们可以执行的操作。

Checkpointing

您通常要使用的另一个TensorFlow功能是Checkpointing - 保存模型的参数以便以后恢复它们。Checkpointing可用于稍后继续训练,或使用提前停止选择最佳参数设置。使用Saver对象创建检查点。

1
2
3
4
5
6
7
# Checkpointing
checkpoint_dir = os.path.abspath(os.path.join(out_dir, "checkpoints"))
checkpoint_prefix = os.path.join(checkpoint_dir, "model")
# Tensorflow assumes this directory already exists so we need to create it
if not os.path.exists(checkpoint_dir):
os.makedirs(checkpoint_dir)
saver = tf.train.Saver(tf.all_variables())

Initializing the variables

在我们训练模型之前,我们还需要在图中初始化变量。

1
sess.run(tf.initialize_all_variables())

该initialize_all_variables功能是一个方便的功能,运行所有我们为我们的变量定义的初始化的。您也可以手动调用变量的初始值设定项。如果您想要使用预先训练的值初始化嵌入,这非常有用。

Defining a single training step

现在让我们为单个训练步骤定义一个函数,在一批数据上评估模型并更新模型参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def train_step(x_batch, y_batch):
"""
A single training step
"""
feed_dict = {
cnn.input_x: x_batch,
cnn.input_y: y_batch,
cnn.dropout_keep_prob: FLAGS.dropout_keep_prob
}
_, step, summaries, loss, accuracy = sess.run(
[train_op, global_step, train_summary_op, cnn.loss, cnn.accuracy],
feed_dict)
time_str = datetime.datetime.now().isoformat()
print("{}: step {}, loss {:g}, acc {:g}".format(time_str, step, loss, accuracy))
train_summary_writer.add_summary(summaries, step)

feed_dict包含我们传递给网络的占位符节点的数据。您必须为所有占位符节点提供值,否则TensorFlow将引发错误。处理输入数据的另一种方法是使用队列,但这超出了本文的范围。

接下来,我们执行train_opusing session.run,它返回我们要求它评估的所有操作的值。请注意,train_op什么都不返回,它只是更新我们网络的参数。最后,我们打印当前培训批次的丢失和准确性,并将摘要保存到磁盘。请注意,如果批次较小,批次培训批次的损失和准确性可能会有很大差异。由于我们使用的是辍学,因此您的培训指标可能会比评估指标更差。

我们编写了一个类似的函数来评估任意数据集的损失和准确性,例如验证集或整个训练集。基本上这个功能与上面的功能相同,但没有训练操作。它还会禁用丢失。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def dev_step(x_batch, y_batch, writer=None):
"""
Evaluates model on a dev set
"""
feed_dict = {
cnn.input_x: x_batch,
cnn.input_y: y_batch,
cnn.dropout_keep_prob: 1.0
}
step, summaries, loss, accuracy = sess.run(
[global_step, dev_summary_op, cnn.loss, cnn.accuracy],
feed_dict)
time_str = datetime.datetime.now().isoformat()
print("{}: step {}, loss {:g}, acc {:g}".format(time_str, step, loss, accuracy))
if writer:
writer.add_summary(summaries, step)

Training loop

最后,我们准备编写训练循环了。我们迭代批量数据,train_step为每个批次调用函数,不定时评估和检查我们的模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Generate batches
batches = data_helpers.batch_iter(
zip(x_train, y_train), FLAGS.batch_size, FLAGS.num_epochs)
# Training loop. For each batch...
for batch in batches:
x_batch, y_batch = zip(*batch)
train_step(x_batch, y_batch)
current_step = tf.train.global_step(sess, global_step)
if current_step % FLAGS.evaluate_every == 0:
print("\nEvaluation:")
dev_step(x_dev, y_dev, writer=dev_summary_writer)
print("")
if current_step % FLAGS.checkpoint_every == 0:
path = saver.save(sess, checkpoint_prefix, global_step=current_step)
print("Saved model checkpoint to {}\n".format(path))

这里,batch_iter是我编写的批处理数据的辅助函数,tf.train.global_step是返回值的便捷函数global_step。
此处提供完整的训练代码